aboutsummaryrefslogtreecommitdiff
path: root/src/app/api/websites/[websiteId]
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/api/websites/[websiteId]')
-rw-r--r--src/app/api/websites/[websiteId]/active/route.ts25
-rw-r--r--src/app/api/websites/[websiteId]/daterange/route.ts25
-rw-r--r--src/app/api/websites/[websiteId]/event-data/[eventId]/route.ts25
-rw-r--r--src/app/api/websites/[websiteId]/event-data/events/route.ts37
-rw-r--r--src/app/api/websites/[websiteId]/event-data/fields/route.ts35
-rw-r--r--src/app/api/websites/[websiteId]/event-data/properties/route.ts35
-rw-r--r--src/app/api/websites/[websiteId]/event-data/stats/route.ts35
-rw-r--r--src/app/api/websites/[websiteId]/event-data/values/route.ts41
-rw-r--r--src/app/api/websites/[websiteId]/events/route.ts37
-rw-r--r--src/app/api/websites/[websiteId]/events/series/route.ts37
-rw-r--r--src/app/api/websites/[websiteId]/export/route.ts64
-rw-r--r--src/app/api/websites/[websiteId]/metrics/expanded/route.ts66
-rw-r--r--src/app/api/websites/[websiteId]/metrics/route.ts66
-rw-r--r--src/app/api/websites/[websiteId]/pageviews/route.ts72
-rw-r--r--src/app/api/websites/[websiteId]/reports/route.ts46
-rw-r--r--src/app/api/websites/[websiteId]/reset/route.ts25
-rw-r--r--src/app/api/websites/[websiteId]/route.ts84
-rw-r--r--src/app/api/websites/[websiteId]/segments/[segmentId]/route.ts92
-rw-r--r--src/app/api/websites/[websiteId]/segments/route.ts70
-rw-r--r--src/app/api/websites/[websiteId]/session-data/properties/route.ts35
-rw-r--r--src/app/api/websites/[websiteId]/session-data/values/route.ts40
-rw-r--r--src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts33
-rw-r--r--src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts25
-rw-r--r--src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts25
-rw-r--r--src/app/api/websites/[websiteId]/sessions/route.ts36
-rw-r--r--src/app/api/websites/[websiteId]/sessions/stats/route.ts42
-rw-r--r--src/app/api/websites/[websiteId]/sessions/weekly/route.ts36
-rw-r--r--src/app/api/websites/[websiteId]/stats/route.ts43
-rw-r--r--src/app/api/websites/[websiteId]/transfer/route.ts50
-rw-r--r--src/app/api/websites/[websiteId]/values/route.ts50
30 files changed, 1332 insertions, 0 deletions
diff --git a/src/app/api/websites/[websiteId]/active/route.ts b/src/app/api/websites/[websiteId]/active/route.ts
new file mode 100644
index 0000000..233b97e
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/active/route.ts
@@ -0,0 +1,25 @@
+import { parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { canViewWebsite } from '@/permissions';
+import { getActiveVisitors } from '@/queries/sql';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const { auth, error } = await parseRequest(request);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const visitors = await getActiveVisitors(websiteId);
+
+ return json(visitors);
+}
diff --git a/src/app/api/websites/[websiteId]/daterange/route.ts b/src/app/api/websites/[websiteId]/daterange/route.ts
new file mode 100644
index 0000000..14a241f
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/daterange/route.ts
@@ -0,0 +1,25 @@
+import { parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { canViewWebsite } from '@/permissions';
+import { getWebsiteDateRange } from '@/queries/sql';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const { auth, error } = await parseRequest(request);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const dateRange = await getWebsiteDateRange(websiteId);
+
+ return json(dateRange);
+}
diff --git a/src/app/api/websites/[websiteId]/event-data/[eventId]/route.ts b/src/app/api/websites/[websiteId]/event-data/[eventId]/route.ts
new file mode 100644
index 0000000..54afab2
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/event-data/[eventId]/route.ts
@@ -0,0 +1,25 @@
+import { parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { canViewWebsite } from '@/permissions';
+import { getEventData } from '@/queries/sql/events/getEventData';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string; eventId: string }> },
+) {
+ const { auth, error } = await parseRequest(request);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId, eventId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const data = await getEventData(websiteId, eventId);
+
+ return json(data);
+}
diff --git a/src/app/api/websites/[websiteId]/event-data/events/route.ts b/src/app/api/websites/[websiteId]/event-data/events/route.ts
new file mode 100644
index 0000000..eb6ee6e
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/event-data/events/route.ts
@@ -0,0 +1,37 @@
+import { z } from 'zod';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { filterParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getEventDataEvents } from '@/queries/sql/events/getEventDataEvents';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ startAt: z.coerce.number().int(),
+ endAt: z.coerce.number().int(),
+ event: z.string().optional(),
+ ...filterParams,
+ });
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const filters = await getQueryFilters(query, websiteId);
+
+ const data = await getEventDataEvents(websiteId, {
+ ...filters,
+ });
+
+ return json(data);
+}
diff --git a/src/app/api/websites/[websiteId]/event-data/fields/route.ts b/src/app/api/websites/[websiteId]/event-data/fields/route.ts
new file mode 100644
index 0000000..bce6a97
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/event-data/fields/route.ts
@@ -0,0 +1,35 @@
+import { z } from 'zod';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { filterParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getEventDataFields } from '@/queries/sql';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ startAt: z.coerce.number().int(),
+ endAt: z.coerce.number().int(),
+ ...filterParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const filters = await getQueryFilters(query, websiteId);
+
+ const data = await getEventDataFields(websiteId, filters);
+
+ return json(data);
+}
diff --git a/src/app/api/websites/[websiteId]/event-data/properties/route.ts b/src/app/api/websites/[websiteId]/event-data/properties/route.ts
new file mode 100644
index 0000000..52d15cf
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/event-data/properties/route.ts
@@ -0,0 +1,35 @@
+import { z } from 'zod';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { filterParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getEventDataProperties } from '@/queries/sql';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ startAt: z.coerce.number().int(),
+ endAt: z.coerce.number().int(),
+ ...filterParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const filters = await getQueryFilters(query, websiteId);
+
+ const data = await getEventDataProperties(websiteId, filters);
+
+ return json(data);
+}
diff --git a/src/app/api/websites/[websiteId]/event-data/stats/route.ts b/src/app/api/websites/[websiteId]/event-data/stats/route.ts
new file mode 100644
index 0000000..042e989
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/event-data/stats/route.ts
@@ -0,0 +1,35 @@
+import { z } from 'zod';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { filterParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getEventDataStats } from '@/queries/sql';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ startAt: z.coerce.number().int(),
+ endAt: z.coerce.number().int(),
+ ...filterParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const filters = await getQueryFilters(query, websiteId);
+
+ const data = await getEventDataStats(websiteId, filters);
+
+ return json(data);
+}
diff --git a/src/app/api/websites/[websiteId]/event-data/values/route.ts b/src/app/api/websites/[websiteId]/event-data/values/route.ts
new file mode 100644
index 0000000..12e8f2d
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/event-data/values/route.ts
@@ -0,0 +1,41 @@
+import { z } from 'zod';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { filterParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getEventDataValues } from '@/queries/sql';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ startAt: z.coerce.number().int(),
+ endAt: z.coerce.number().int(),
+ event: z.string(),
+ propertyName: z.string(),
+ ...filterParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const { propertyName } = query;
+ const filters = await getQueryFilters(query, websiteId);
+
+ const data = await getEventDataValues(websiteId, {
+ ...filters,
+ propertyName,
+ });
+
+ return json(data);
+}
diff --git a/src/app/api/websites/[websiteId]/events/route.ts b/src/app/api/websites/[websiteId]/events/route.ts
new file mode 100644
index 0000000..74ec3ec
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/events/route.ts
@@ -0,0 +1,37 @@
+import { z } from 'zod';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { filterParams, pagingParams, searchParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getWebsiteEvents } from '@/queries/sql';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ startAt: z.coerce.number().optional(),
+ endAt: z.coerce.number().optional(),
+ ...filterParams,
+ ...pagingParams,
+ ...searchParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const filters = await getQueryFilters(query, websiteId);
+
+ const data = await getWebsiteEvents(websiteId, filters);
+
+ return json(data);
+}
diff --git a/src/app/api/websites/[websiteId]/events/series/route.ts b/src/app/api/websites/[websiteId]/events/series/route.ts
new file mode 100644
index 0000000..977e9c8
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/events/series/route.ts
@@ -0,0 +1,37 @@
+import { z } from 'zod';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { filterParams, timezoneParam, unitParam } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getEventStats } from '@/queries/sql';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ startAt: z.coerce.number().int(),
+ endAt: z.coerce.number().int(),
+ unit: unitParam.optional(),
+ timezone: timezoneParam,
+ ...filterParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const filters = await getQueryFilters(query, websiteId);
+
+ const data = await getEventStats(websiteId, filters);
+
+ return json(data);
+}
diff --git a/src/app/api/websites/[websiteId]/export/route.ts b/src/app/api/websites/[websiteId]/export/route.ts
new file mode 100644
index 0000000..eec81c6
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/export/route.ts
@@ -0,0 +1,64 @@
+import JSZip from 'jszip';
+import Papa from 'papaparse';
+import { z } from 'zod';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { dateRangeParams, pagingParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getEventMetrics, getPageviewMetrics, getSessionMetrics } from '@/queries/sql';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ ...dateRangeParams,
+ ...pagingParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const filters = await getQueryFilters(query, websiteId);
+
+ const [events, pages, referrers, browsers, os, devices, countries] = await Promise.all([
+ getEventMetrics(websiteId, { type: 'event' }, filters),
+ getPageviewMetrics(websiteId, { type: 'path' }, filters),
+ getPageviewMetrics(websiteId, { type: 'referrer' }, filters),
+ getSessionMetrics(websiteId, { type: 'browser' }, filters),
+ getSessionMetrics(websiteId, { type: 'os' }, filters),
+ getSessionMetrics(websiteId, { type: 'device' }, filters),
+ getSessionMetrics(websiteId, { type: 'country' }, filters),
+ ]);
+
+ const zip = new JSZip();
+
+ const parse = (data: any) => {
+ return Papa.unparse(data, {
+ header: true,
+ skipEmptyLines: true,
+ });
+ };
+
+ zip.file('events.csv', parse(events));
+ zip.file('pages.csv', parse(pages));
+ zip.file('referrers.csv', parse(referrers));
+ zip.file('browsers.csv', parse(browsers));
+ zip.file('os.csv', parse(os));
+ zip.file('devices.csv', parse(devices));
+ zip.file('countries.csv', parse(countries));
+
+ const content = await zip.generateAsync({ type: 'nodebuffer' });
+ const base64 = content.toString('base64');
+
+ return json({ zip: base64 });
+}
diff --git a/src/app/api/websites/[websiteId]/metrics/expanded/route.ts b/src/app/api/websites/[websiteId]/metrics/expanded/route.ts
new file mode 100644
index 0000000..d52c177
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/metrics/expanded/route.ts
@@ -0,0 +1,66 @@
+import { z } from 'zod';
+import { EVENT_COLUMNS, EVENT_TYPE, SESSION_COLUMNS } from '@/lib/constants';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { badRequest, json, unauthorized } from '@/lib/response';
+import { dateRangeParams, filterParams, searchParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import {
+ getChannelExpandedMetrics,
+ getEventExpandedMetrics,
+ getPageviewExpandedMetrics,
+ getSessionExpandedMetrics,
+} from '@/queries/sql';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ type: z.string(),
+ limit: z.coerce.number().optional(),
+ offset: z.coerce.number().optional(),
+ ...dateRangeParams,
+ ...searchParams,
+ ...filterParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const { type, limit, offset, search } = query;
+ const filters = await getQueryFilters(query, websiteId);
+
+ if (search) {
+ filters[type] = `c.${search}`;
+ }
+
+ if (SESSION_COLUMNS.includes(type)) {
+ const data = await getSessionExpandedMetrics(websiteId, { type, limit, offset }, filters);
+
+ return json(data);
+ }
+
+ if (EVENT_COLUMNS.includes(type)) {
+ if (type === 'event') {
+ filters.eventType = EVENT_TYPE.customEvent;
+ return json(await getEventExpandedMetrics(websiteId, { type, limit, offset }, filters));
+ } else {
+ return json(await getPageviewExpandedMetrics(websiteId, { type, limit, offset }, filters));
+ }
+ }
+
+ if (type === 'channel') {
+ return json(await getChannelExpandedMetrics(websiteId, filters));
+ }
+
+ return badRequest();
+}
diff --git a/src/app/api/websites/[websiteId]/metrics/route.ts b/src/app/api/websites/[websiteId]/metrics/route.ts
new file mode 100644
index 0000000..12784ad
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/metrics/route.ts
@@ -0,0 +1,66 @@
+import { z } from 'zod';
+import { EVENT_COLUMNS, EVENT_TYPE, SESSION_COLUMNS } from '@/lib/constants';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { badRequest, json, unauthorized } from '@/lib/response';
+import { dateRangeParams, filterParams, searchParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import {
+ getChannelMetrics,
+ getEventMetrics,
+ getPageviewMetrics,
+ getSessionMetrics,
+} from '@/queries/sql';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ type: z.string(),
+ limit: z.coerce.number().optional(),
+ offset: z.coerce.number().optional(),
+ ...dateRangeParams,
+ ...searchParams,
+ ...filterParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const { type, limit, offset, search } = query;
+ const filters = await getQueryFilters(query, websiteId);
+
+ if (search) {
+ filters[type] = `c.${search}`;
+ }
+
+ if (SESSION_COLUMNS.includes(type)) {
+ const data = await getSessionMetrics(websiteId, { type, limit, offset }, filters);
+
+ return json(data);
+ }
+
+ if (EVENT_COLUMNS.includes(type)) {
+ if (type === 'event') {
+ filters.eventType = EVENT_TYPE.customEvent;
+ return json(await getEventMetrics(websiteId, { type, limit, offset }, filters));
+ } else {
+ return json(await getPageviewMetrics(websiteId, { type, limit, offset }, filters));
+ }
+ }
+
+ if (type === 'channel') {
+ return json(await getChannelMetrics(websiteId, filters));
+ }
+
+ return badRequest();
+}
diff --git a/src/app/api/websites/[websiteId]/pageviews/route.ts b/src/app/api/websites/[websiteId]/pageviews/route.ts
new file mode 100644
index 0000000..af59bce
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/pageviews/route.ts
@@ -0,0 +1,72 @@
+import { z } from 'zod';
+import { getCompareDate } from '@/lib/date';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { dateRangeParams, filterParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getPageviewStats, getSessionStats } from '@/queries/sql';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ ...dateRangeParams,
+ ...filterParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const filters = await getQueryFilters(query, websiteId);
+
+ const [pageviews, sessions] = await Promise.all([
+ getPageviewStats(websiteId, filters),
+ getSessionStats(websiteId, filters),
+ ]);
+
+ if (filters.compare) {
+ const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate(
+ filters.compare,
+ filters.startDate,
+ filters.endDate,
+ );
+
+ const [comparePageviews, compareSessions] = await Promise.all([
+ getPageviewStats(websiteId, {
+ ...filters,
+ startDate: compareStartDate,
+ endDate: compareEndDate,
+ }),
+ getSessionStats(websiteId, {
+ ...filters,
+ startDate: compareStartDate,
+ endDate: compareEndDate,
+ }),
+ ]);
+
+ return json({
+ pageviews,
+ sessions,
+ startDate: filters.startDate,
+ endDate: filters.endDate,
+ compare: {
+ pageviews: comparePageviews,
+ sessions: compareSessions,
+ startDate: compareStartDate,
+ endDate: compareEndDate,
+ },
+ });
+ }
+
+ return json({ pageviews, sessions });
+}
diff --git a/src/app/api/websites/[websiteId]/reports/route.ts b/src/app/api/websites/[websiteId]/reports/route.ts
new file mode 100644
index 0000000..93e7ab4
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/reports/route.ts
@@ -0,0 +1,46 @@
+import { z } from 'zod';
+import { parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { filterParams, pagingParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getReports } from '@/queries/prisma';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+ filters: { type: string },
+) {
+ const schema = z.object({
+ ...filterParams,
+ ...pagingParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+ const { page, pageSize, search } = query;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const data = await getReports(
+ {
+ where: {
+ websiteId,
+ type: filters.type,
+ },
+ },
+ {
+ page,
+ pageSize,
+ search,
+ },
+ );
+
+ return json(data);
+}
diff --git a/src/app/api/websites/[websiteId]/reset/route.ts b/src/app/api/websites/[websiteId]/reset/route.ts
new file mode 100644
index 0000000..e0be5a5
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/reset/route.ts
@@ -0,0 +1,25 @@
+import { parseRequest } from '@/lib/request';
+import { ok, unauthorized } from '@/lib/response';
+import { canUpdateWebsite } from '@/permissions';
+import { resetWebsite } from '@/queries/prisma';
+
+export async function POST(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const { auth, error } = await parseRequest(request);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+
+ if (!(await canUpdateWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ await resetWebsite(websiteId);
+
+ return ok();
+}
diff --git a/src/app/api/websites/[websiteId]/route.ts b/src/app/api/websites/[websiteId]/route.ts
new file mode 100644
index 0000000..b4c0e7e
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/route.ts
@@ -0,0 +1,84 @@
+import { z } from 'zod';
+import { SHARE_ID_REGEX } from '@/lib/constants';
+import { parseRequest } from '@/lib/request';
+import { badRequest, json, ok, serverError, unauthorized } from '@/lib/response';
+import { canDeleteWebsite, canUpdateWebsite, canViewWebsite } from '@/permissions';
+import { deleteWebsite, getWebsite, updateWebsite } from '@/queries/prisma';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const { auth, error } = await parseRequest(request);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const website = await getWebsite(websiteId);
+
+ return json(website);
+}
+
+export async function POST(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ name: z.string().optional(),
+ domain: z.string().optional(),
+ shareId: z.string().regex(SHARE_ID_REGEX).nullable().optional(),
+ });
+
+ const { auth, body, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+ const { name, domain, shareId } = body;
+
+ if (!(await canUpdateWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ try {
+ const website = await updateWebsite(websiteId, { name, domain, shareId });
+
+ return Response.json(website);
+ } catch (e: any) {
+ if (e.message.toLowerCase().includes('unique constraint') && e.message.includes('share_id')) {
+ return badRequest({ message: 'That share ID is already taken.' });
+ }
+
+ return serverError(e);
+ }
+}
+
+export async function DELETE(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const { auth, error } = await parseRequest(request);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+
+ if (!(await canDeleteWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ await deleteWebsite(websiteId);
+
+ return ok();
+}
diff --git a/src/app/api/websites/[websiteId]/segments/[segmentId]/route.ts b/src/app/api/websites/[websiteId]/segments/[segmentId]/route.ts
new file mode 100644
index 0000000..b51f783
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/segments/[segmentId]/route.ts
@@ -0,0 +1,92 @@
+import { z } from 'zod';
+import { parseRequest } from '@/lib/request';
+import { json, notFound, ok, unauthorized } from '@/lib/response';
+import { anyObjectParam, segmentTypeParam } from '@/lib/schema';
+import { canDeleteWebsite, canUpdateWebsite, canViewWebsite } from '@/permissions';
+import { deleteSegment, getSegment, updateSegment } from '@/queries/prisma';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string; segmentId: string }> },
+) {
+ const { auth, error } = await parseRequest(request);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId, segmentId } = await params;
+
+ const segment = await getSegment(segmentId);
+
+ if (websiteId && !(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ return json(segment);
+}
+
+export async function POST(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string; segmentId: string }> },
+) {
+ const schema = z.object({
+ type: segmentTypeParam,
+ name: z.string().max(200),
+ parameters: anyObjectParam,
+ });
+
+ const { auth, body, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId, segmentId } = await params;
+ const { type, name, parameters } = body;
+
+ const segment = await getSegment(segmentId);
+
+ if (!segment) {
+ return notFound();
+ }
+
+ if (!(await canUpdateWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const result = await updateSegment(segmentId, {
+ type,
+ name,
+ parameters,
+ } as any);
+
+ return json(result);
+}
+
+export async function DELETE(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string; segmentId: string }> },
+) {
+ const { auth, error } = await parseRequest(request);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId, segmentId } = await params;
+
+ const segment = await getSegment(segmentId);
+
+ if (!segment) {
+ return notFound();
+ }
+
+ if (!(await canDeleteWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ await deleteSegment(segmentId);
+
+ return ok();
+}
diff --git a/src/app/api/websites/[websiteId]/segments/route.ts b/src/app/api/websites/[websiteId]/segments/route.ts
new file mode 100644
index 0000000..4592765
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/segments/route.ts
@@ -0,0 +1,70 @@
+import { z } from 'zod';
+import { uuid } from '@/lib/crypto';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { anyObjectParam, searchParams, segmentTypeParam } from '@/lib/schema';
+import { canUpdateWebsite, canViewWebsite } from '@/permissions';
+import { createSegment, getWebsiteSegments } from '@/queries/prisma';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ type: segmentTypeParam,
+ ...searchParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+ const { type } = query;
+
+ if (websiteId && !(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const filters = await getQueryFilters(query);
+
+ const segments = await getWebsiteSegments(websiteId, type, filters);
+
+ return json(segments);
+}
+
+export async function POST(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ type: segmentTypeParam,
+ name: z.string().max(200),
+ parameters: anyObjectParam,
+ });
+
+ const { auth, body, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+ const { type, name, parameters } = body;
+
+ if (!(await canUpdateWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const result = await createSegment({
+ id: uuid(),
+ websiteId,
+ type,
+ name,
+ parameters,
+ } as any);
+
+ return json(result);
+}
diff --git a/src/app/api/websites/[websiteId]/session-data/properties/route.ts b/src/app/api/websites/[websiteId]/session-data/properties/route.ts
new file mode 100644
index 0000000..2d8db15
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/session-data/properties/route.ts
@@ -0,0 +1,35 @@
+import { z } from 'zod';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { filterParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getSessionDataProperties } from '@/queries/sql';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ startAt: z.coerce.number().int(),
+ endAt: z.coerce.number().int(),
+ ...filterParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const filters = await getQueryFilters(query, websiteId);
+
+ const data = await getSessionDataProperties(websiteId, filters);
+
+ return json(data);
+}
diff --git a/src/app/api/websites/[websiteId]/session-data/values/route.ts b/src/app/api/websites/[websiteId]/session-data/values/route.ts
new file mode 100644
index 0000000..7d06870
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/session-data/values/route.ts
@@ -0,0 +1,40 @@
+import { z } from 'zod';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { filterParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getSessionDataValues } from '@/queries/sql';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ startAt: z.coerce.number().int(),
+ endAt: z.coerce.number().int(),
+ propertyName: z.string().optional(),
+ ...filterParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const { propertyName } = query;
+ const filters = await getQueryFilters(query, websiteId);
+
+ const data = await getSessionDataValues(websiteId, {
+ ...filters,
+ propertyName,
+ });
+
+ return json(data);
+}
diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts
new file mode 100644
index 0000000..41b766d
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts
@@ -0,0 +1,33 @@
+import { z } from 'zod';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { canViewWebsite } from '@/permissions';
+import { getSessionActivity } from '@/queries/sql';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string; sessionId: string }> },
+) {
+ const schema = z.object({
+ startAt: z.coerce.number().int(),
+ endAt: z.coerce.number().int(),
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId, sessionId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const filters = await getQueryFilters(query, websiteId);
+
+ const data = await getSessionActivity(websiteId, sessionId, filters);
+
+ return json(data);
+}
diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts
new file mode 100644
index 0000000..6b5c241
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts
@@ -0,0 +1,25 @@
+import { parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { canViewWebsite } from '@/permissions';
+import { getSessionData } from '@/queries/sql';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string; sessionId: string }> },
+) {
+ const { auth, error } = await parseRequest(request);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId, sessionId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const data = await getSessionData(websiteId, sessionId);
+
+ return json(data);
+}
diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts
new file mode 100644
index 0000000..1091663
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts
@@ -0,0 +1,25 @@
+import { parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { canViewWebsite } from '@/permissions';
+import { getWebsiteSession } from '@/queries/sql';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string; sessionId: string }> },
+) {
+ const { auth, error } = await parseRequest(request);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId, sessionId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const data = await getWebsiteSession(websiteId, sessionId);
+
+ return json(data);
+}
diff --git a/src/app/api/websites/[websiteId]/sessions/route.ts b/src/app/api/websites/[websiteId]/sessions/route.ts
new file mode 100644
index 0000000..ed4757a
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/sessions/route.ts
@@ -0,0 +1,36 @@
+import { z } from 'zod';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { dateRangeParams, filterParams, pagingParams, searchParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getWebsiteSessions } from '@/queries/sql';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ ...dateRangeParams,
+ ...filterParams,
+ ...pagingParams,
+ ...searchParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const filters = await getQueryFilters(query, websiteId);
+
+ const data = await getWebsiteSessions(websiteId, filters);
+
+ return json(data);
+}
diff --git a/src/app/api/websites/[websiteId]/sessions/stats/route.ts b/src/app/api/websites/[websiteId]/sessions/stats/route.ts
new file mode 100644
index 0000000..459830e
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/sessions/stats/route.ts
@@ -0,0 +1,42 @@
+import { z } from 'zod';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { filterParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getWebsiteSessionStats } from '@/queries/sql';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ startAt: z.coerce.number().int(),
+ endAt: z.coerce.number().int(),
+ ...filterParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const filters = await getQueryFilters(query, websiteId);
+
+ const metrics = await getWebsiteSessionStats(websiteId, filters);
+
+ const data = Object.keys(metrics[0]).reduce((obj, key) => {
+ obj[key] = {
+ value: Number(metrics[0][key]) || 0,
+ };
+ return obj;
+ }, {});
+
+ return json(data);
+}
diff --git a/src/app/api/websites/[websiteId]/sessions/weekly/route.ts b/src/app/api/websites/[websiteId]/sessions/weekly/route.ts
new file mode 100644
index 0000000..b9ccf3e
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/sessions/weekly/route.ts
@@ -0,0 +1,36 @@
+import { z } from 'zod';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { filterParams, timezoneParam } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getWeeklyTraffic } from '@/queries/sql';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ startAt: z.coerce.number().int(),
+ endAt: z.coerce.number().int(),
+ timezone: timezoneParam,
+ ...filterParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const filters = await getQueryFilters(query, websiteId);
+
+ const data = await getWeeklyTraffic(websiteId, filters);
+
+ return json(data);
+}
diff --git a/src/app/api/websites/[websiteId]/stats/route.ts b/src/app/api/websites/[websiteId]/stats/route.ts
new file mode 100644
index 0000000..07c8b96
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/stats/route.ts
@@ -0,0 +1,43 @@
+import { z } from 'zod';
+import { getCompareDate } from '@/lib/date';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { dateRangeParams, filterParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getWebsiteStats } from '@/queries/sql';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ ...dateRangeParams,
+ ...filterParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const filters = await getQueryFilters(query, websiteId);
+
+ const data = await getWebsiteStats(websiteId, filters);
+
+ const { startDate, endDate } = getCompareDate('prev', filters.startDate, filters.endDate);
+
+ const comparison = await getWebsiteStats(websiteId, {
+ ...filters,
+ startDate,
+ endDate,
+ });
+
+ return json({ ...data, comparison });
+}
diff --git a/src/app/api/websites/[websiteId]/transfer/route.ts b/src/app/api/websites/[websiteId]/transfer/route.ts
new file mode 100644
index 0000000..df2fed2
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/transfer/route.ts
@@ -0,0 +1,50 @@
+import { z } from 'zod';
+import { parseRequest } from '@/lib/request';
+import { badRequest, json, unauthorized } from '@/lib/response';
+import { canTransferWebsiteToTeam, canTransferWebsiteToUser } from '@/permissions';
+import { updateWebsite } from '@/queries/prisma';
+
+export async function POST(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ userId: z.uuid().optional(),
+ teamId: z.uuid().optional(),
+ });
+
+ const { auth, body, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+ const { userId, teamId } = body;
+
+ if (userId) {
+ if (!(await canTransferWebsiteToUser(auth, websiteId, userId))) {
+ return unauthorized();
+ }
+
+ const website = await updateWebsite(websiteId, {
+ userId,
+ teamId: null,
+ });
+
+ return json(website);
+ } else if (teamId) {
+ if (!(await canTransferWebsiteToTeam(auth, websiteId, teamId))) {
+ return unauthorized();
+ }
+
+ const website = await updateWebsite(websiteId, {
+ userId: null,
+ teamId,
+ });
+
+ return json(website);
+ }
+
+ return badRequest();
+}
diff --git a/src/app/api/websites/[websiteId]/values/route.ts b/src/app/api/websites/[websiteId]/values/route.ts
new file mode 100644
index 0000000..172325e
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/values/route.ts
@@ -0,0 +1,50 @@
+import { z } from 'zod';
+import { EVENT_COLUMNS, FILTER_COLUMNS, SEGMENT_TYPES, SESSION_COLUMNS } from '@/lib/constants';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { badRequest, json, unauthorized } from '@/lib/response';
+import { dateRangeParams, fieldsParam, searchParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getWebsiteSegments } from '@/queries/prisma';
+import { getValues } from '@/queries/sql';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ type: fieldsParam,
+ ...dateRangeParams,
+ ...searchParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const { type } = query;
+
+ if (!SESSION_COLUMNS.includes(type) && !EVENT_COLUMNS.includes(type) && !SEGMENT_TYPES[type]) {
+ return badRequest();
+ }
+
+ let values: any[];
+
+ if (SEGMENT_TYPES[type]) {
+ values = (await getWebsiteSegments(websiteId, type))?.data?.map(segment => ({
+ value: segment.name,
+ }));
+ } else {
+ const filters = await getQueryFilters(query, websiteId);
+ values = await getValues(websiteId, FILTER_COLUMNS[type], filters);
+ }
+
+ return json(values.filter(n => n).sort());
+}